package xfile

import (
	"errors"
	"io"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"gitee.com/xuesongtao/gotool/base"
	"gitee.com/xuesongtao/gotool/internal"
	"gitee.com/xuesongtao/gotool/xcache"
	"gitee.com/xuesongtao/gotool/xlist"
)

const (
	defaultExpireDur  = 10 * time.Minute // 过期的时间间隔
	defaultMaxFhCount = 3
	pathFlagSplit     = "|||"
)

type Logger interface {
	Info(v ...interface{})
	Infof(format string, v ...interface{})
	Error(v ...interface{})
	Errorf(format string, v ...interface{})
}

var (
	flg Logger = internal.NewLogger()
)

// SetLogger 设置 log
func SetLogger(l Logger) {
	flg = l
}

// FileHandle
type FileHandle struct {
	path       string    // 文件路径
	flag       int       // 文件操作
	f          *os.File  // 文件句柄
	expireTime time.Time // 过期时间
}

// NewFileHandle
func NewFileHandle(path string, expire ...time.Time) *FileHandle {
	obj := &FileHandle{path: path}
	if len(expire) > 0 {
		obj.expireTime = expire[0]
	}
	return obj
}

// IsExpire 过期
func (fh *FileHandle) IsExpire() bool {
	return !fh.expireTime.IsZero() && fh.expireTime.Before(time.Now())
}

// Initf 初始化文件句柄
func (fh *FileHandle) Initf(flag int, perm ...os.FileMode) error {
	defaultPerm := os.FileMode(0666)
	if len(perm) > 0 {
		defaultPerm = perm[0]
	}
	fh.flag = flag

	// 不存在就新建
	if !fh.Exists() {
		f, err := Create(fh.path)
		if err != nil {
			return err
		}
		f.Close()
	}

	var err error
	fh.f, err = os.OpenFile(fh.path, fh.flag, defaultPerm)
	if err != nil {
		return err
	}
	return nil
}

// Exists 判断文件存在不
func (fh *FileHandle) Exists() bool {
	return Exists(fh.path)
}

// GetFile 获取文件操作句柄
func (fh *FileHandle) GetFile(flag ...int) *os.File {
	if fh.f == nil {
		defaultFlag := fh.flag
		if len(flag) > 0 {
			defaultFlag = flag[0]
		}
		if err := fh.Initf(defaultFlag); err != nil {
			flg.Error("fh.Initf is failed, err:", err)
		}
	}
	return fh.f
}

// Close 关闭文件句柄
func (fh *FileHandle) Close() error {
	return fh.f.Close()
}

// ResetSeek 重置油标
func (fh *FileHandle) ResetSeek(seekFlag ...int) (int64, error) {
	defaultSeek := io.SeekStart
	if len(seekFlag) > 0 {
		defaultSeek = seekFlag[0]
	}
	return fh.f.Seek(0, defaultSeek)
}

// Clean 清空文件内容
func (fh *FileHandle) Clean() error {
	if err := fh.f.Truncate(0); err != nil {
		return err
	}

	if _, err := fh.ResetSeek(); err != nil {
		return err
	}
	return nil
}

// GetContent 查询内容
func (fh *FileHandle) GetContent() (string, error) {
	var size int
	if info, err := fh.f.Stat(); err == nil {
		size64 := info.Size()
		if int64(int(size64)) == size64 {
			size = int(size64)
		}
	}
	size++ // one byte for final read at EOF
	if size < 512 {
		size = 512
	}

	// 这里会出现复用情况, 需要重置下油标
	fh.ResetSeek()
	data := make([]byte, 0, size)
	for {
		if len(data) >= cap(data) {
			d := append(data[:cap(data)], 0)
			data = d[:len(data)]
		}
		n, err := fh.f.Read(data[len(data):cap(data)])
		data = data[:len(data)+n]
		if err != nil {
			if err == io.EOF {
				break
			}
			return "", err
		}
	}
	return string(data), nil
}

// PutContent 向文件里写内容, 只会覆写
func (fh *FileHandle) PutContent(content string) (int, error) {
	if fh.flag&os.O_APPEND > 0 {
		return 0, errors.New("flag no should has os.O_APPEND")
	}
	if fh.flag&os.O_TRUNC == 0 {
		return 0, errors.New("flag no has os.O_TRUNC")
	}
	fh.Clean()
	return fh.f.WriteString(content)
}

// AppendContent 向文件写入内容, 内容追加
func (fh *FileHandle) AppendContent(content string) (int, error) {
	if fh.flag&os.O_APPEND == 0 {
		return 0, errors.New("flag no has os.O_APPEND")
	}
	fh.ResetSeek(io.SeekEnd)
	return fh.f.WriteString(content)
}

// FilePool 文件池
type FilePool struct {
	print        bool
	maxFhCount   int            // 相同的 path 最大的 *FileHandle 的数量, 即 fileCache 中 value 的最大数
	file2OpenMap map[string]int // 记录池中文件对应的打开句柄数, key: poolPath  value: 文件句柄打开数
	rw           sync.RWMutex
	fileCache    *xcache.LRUCache // key: poolPath  value: xlist.DList, xlist.DList 值为 *FileHandle
}

// NewFilePool 初始化文件池, 最大 256 个
// 默认会按 defaultExpireDur 进行淘汰
// 注: 使用结束需要调用 Close 释放还在内存中的句柄
func NewFilePool(size ...int) *FilePool {
	defaultSize := 1 << 2
	if len(size) > 0 && (size[0] > 0 && size[0] < defaultSize) {
		defaultSize = size[0]
	}

	obj := &FilePool{
		print:        true,
		maxFhCount:   defaultMaxFhCount,
		file2OpenMap: make(map[string]int),
		fileCache:    xcache.NewLRU(defaultSize),
	}

	// lru 删除回调是否文件句柄
	obj.fileCache.SetDelCallBackFn(func(_, cacheList interface{}) {
		fhs := cacheList.(*xlist.DList)
		for {
			node := fhs.LPop()
			if node == nil {
				break
			}
			obj.closeFh(node.Data.(*FileHandle), "del callback")
		}
	})
	return obj
}

// MaxFhCount 设置相同 path 时, 最大文件句柄数
func (fp *FilePool) MaxFhCount(count int) *FilePool {
	fp.maxFhCount = count
	return fp
}

// Print 是否打印 log level 为 info 的 log
func (fp *FilePool) Print(is bool) *FilePool {
	fp.print = is
	return fp
}

// Get 获取文件句柄; 如果不存在就会新打开文件句柄; 如果文件/目录不存在会自动创建
// 注: 1. 调用完需要调用 Put 才能进行复用, 同时句柄不能 Close
//  2. 如果打算不用了的话, 必须调用 *FileHandle.Close 方法进行释放句柄, 防止内存泄露
func (fp *FilePool) Get(path string, flag int, perm ...os.FileMode) (*FileHandle, error) {
	poolPath := fp.getPoolPath(path, flag)
	fp.rw.RLock()
	fh := fp.getCacheFh(poolPath)
	fp.rw.RUnlock()
	if fh == nil { // 空的话就创建一个
		if fp.print {
			flg.Infof("[%s] getCacheFh fh is nil, it plan new fh", poolPath)
		}
		expireAt := time.Now().Add(defaultExpireDur)
		fh := NewFileHandle(path, expireAt)
		fh.flag = flag
		if err := fh.Initf(fh.flag, perm...); err != nil {
			return nil, err
		}
		return fh, nil
	}
	return fh, nil
}

// getPoolPath 用 路径和flag 区别不同的句柄操作
func (fp *FilePool) getPoolPath(path string, flag int) string {
	// 将 path 和 flag 进行拼接, 如: ./tmp/test.log|||522
	return filepath.Clean(path) + pathFlagSplit + base.ToString(flag)
}

// Remove 移除
func (fp *FilePool) Remove(path string) {
	path = filepath.Clean(path)
	for _, poolPath := range fp.fileCache.Keys() {
		if strings.HasPrefix(poolPath.(string), path) {
			fp.fileCache.Delete(poolPath)
		}
	}
}

// getPath 将 poolPath 转为 path
func (fp *FilePool) getPath(poolPath string) string {
	tmpIndex := strings.LastIndex(poolPath, pathFlagSplit)
	if tmpIndex == -1 {
		return poolPath
	}
	return poolPath[:tmpIndex]
}

// getCacheFh 从缓存中获取操作文件句柄
func (fp *FilePool) getCacheFh(poolPath string) *FileHandle {
	cacheList, ok := fp.fileCache.Load(poolPath)
	if !ok {
		return nil
	}

	fhs := cacheList.(*xlist.DList)
	for {
		node := fhs.LPop()
		if node == nil {
			break
		}

		fh, ok := node.Data.(*FileHandle)
		if ok {
			return fh
		}
		// 如果过期让其返回, 在归还的时候淘汰, 减少初始化
		// if !fp.giveUp(fh) {
		// 	return fh
		// }
	}
	return nil
}

func (fp *FilePool) giveUp(fh *FileHandle) bool {
	if fh.IsExpire() { // 过期了释放下
		fp.fileCache.Delete(fp.getPoolPath(fh.path, fh.flag))
		return true
	}
	return false
}

// closeFh 关闭文件句柄
func (fp *FilePool) closeFh(fh *FileHandle, msg string) {
	poolPath := fp.getPoolPath(fh.path, fh.flag)
	if fp.print {
		flg.Infof("[%s] f is %s, f is closing", poolPath, msg)
	}
	if err := fh.f.Close(); err != nil {
		flg.Error("f.Close is failed, err: %v", poolPath, err)
	}
}

// Put 归还
func (fp *FilePool) Put(fh *FileHandle, expire ...time.Time) {
	if fh.path == "" || fh.f == nil {
		flg.Error("fh is not ok")
		return
	}

	fp.rw.Lock()
	poolPath := fp.getPoolPath(fh.path, fh.flag)
	if fp.giveUp(fh) {
		fp.initFile2OpenMap(poolPath, fh)
		fp.rw.Unlock()
		return
	}
	fp.rw.Unlock()

	// 续期
	fh.expireTime = time.Now().Add(defaultExpireDur)
	if len(expire) > 0 {
		fh.expireTime = expire[0]
	}

	// 装入 cache
	var fhs *xlist.DList
	cacheList, ok := fp.fileCache.Load(poolPath)
	if !ok {
		fhs = xlist.NewDList(true).Append(fh)
	} else {
		fhs = cacheList.(*xlist.DList)
		// 防止相同的的句柄过多的进入缓存
		if fhs.Len() >= fp.maxFhCount {
			fp.closeFh(fh, "full close")
			return
		}
		fhs = fhs.Append(fh)
	}

	fp.rw.Lock()
	fp.initFile2OpenMap(poolPath, fhs)
	fp.rw.Unlock()
	fp.fileCache.Store(poolPath, fhs)
}

func (fp *FilePool) initFile2OpenMap(poolPath string, val interface{}) {
	switch v := val.(type) {
	case *xlist.DList:
		fp.file2OpenMap[poolPath] = v.Len()
	case *FileHandle:
		cacheList, ok := fp.fileCache.Load(poolPath)
		if !ok {
			return
		}
		fp.file2OpenMap[poolPath] = cacheList.(*xlist.DList).Len()
	}
}

// GetFile2OpenMap 获取池中所有文件打开的句柄数
func (fp *FilePool) GetFile2OpenMap() map[string]int {
	fp.rw.RLock()
	defer fp.rw.RUnlock()
	return fp.file2OpenMap
}

// GetFile2Open 获取文件对应的文件句柄打开数
func (fp *FilePool) GetFile2Open(filename string, flag int) int {
	fp.rw.RLock()
	defer fp.rw.RUnlock()
	return fp.file2OpenMap[fp.getPoolPath(filename, flag)]
}

// Close 关闭
func (fp *FilePool) Close() {
	keys := fp.fileCache.Keys()
	for _, key := range keys {
		fp.fileCache.Delete(key)
	}
}
